/**
 * Heterotopia - an up-to-date Java-based MUD system
 * Copyright (C) 2011 Bill Garrett <memesis@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package heterotopia.ss;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;

import org.apache.camel.EndpointInject;
import org.apache.camel.Exchange;
import org.apache.camel.InOnly;
import org.apache.camel.ProducerTemplate;
import org.apache.mina.common.IoFilter;
import org.apache.mina.common.IoSession;

/**
 * MinaHelper manages an IoSession filter and keeps track of opened sessions from a MINA
 * endpoint, allowing asynchronous delivery of messages to sessions. Sessions are
 * identified via UUID, generated by this class.
 * 
 * @author garrett
 */
@InOnly
public class MinaHelper {
	public static String RESERVED_ATTRIBUTE_PREFIX = "org.apache.mina.";
	public static String SESSION_ID_KEY = "sessionId";
	private Map<UUID,IoSession> sessionMap;
	private IoFilter filter;
	private String camelEndpointUri;
	
	@EndpointInject(uri="direct:minaHelper-session")
	protected ProducerTemplate producer;

	/**
	 * Constructs an instance of MinaHelper, specifying the camel endpoint
	 * to which session messages will be sent.
	 * 
	 * @param camelEndpointUri a camel endpoint specified as a URI
	 */
	public MinaHelper(String camelEndpointUri) {
		sessionMap = new HashMap<UUID,IoSession>();
		filter = new MinaSessionTrackerFilter(this);
		this.camelEndpointUri = camelEndpointUri;
	}
	
	/**
	 * Constructs an instance of MinaHelper which by default routes
	 * session messages to "direct:minaHelper-session".
	 */
	public MinaHelper() {
		this("direct:minaHelper-session");
	}
	
	/**
	 * Constructs and returns an IoFilter which can be added
	 * to MINA endpoints.
	 * @return an IoFilter instance
	 */
	public IoFilter getSessionTrackingFilter() {
		return filter;
	}

	/**
	 * Returns the UUIDs of all active sessions.
	 */
	public Set<UUID> sessions() {
		return sessionMap.keySet();
	}
	
	/**
	 * This method is called by the IoFilter to notify the tracker
	 * of a new session.
	 * 
	 * @param session the session which was created
	 * @return a UUID identifying this session
	 */
	public UUID sessionOpened(IoSession session) {
		UUID newSessionId = UUID.randomUUID();
		sessionMap.put(newSessionId, session);
		
		Map<String, String> body = new HashMap<String, String>();
		addSessionAttributesToMap(session, body);
		body.put("command", "");
		body.put(SESSION_ID_KEY, newSessionId.toString());
		body.put("sessionLifecycle", "opened");
		producer.sendBody(camelEndpointUri, body);
		
		return newSessionId;
	}

	/**
	 * This method is called by the IoFilter to notify the tracker
	 * that a session was closed.
	 * 
	 * @param sessionId the session to be removed
	 */
	public void sessionClosed(UUID sessionId) {
		IoSession session = sessionMap.remove(sessionId);
		
		if(null != session) {
			Map<String, String> body = new HashMap<String, String>();
			addSessionAttributesToMap(session, body);
			body.put("command", "");
			body.put(SESSION_ID_KEY, sessionId.toString());
			body.put("sessionLifecycle", "closed");
			producer.sendBody(camelEndpointUri, body);
		}
	}
	
	/**
	 * Given a session ID, return the corresponding IoSession.
	 * 
	 * @param sessionId a unique session ID
	 * @return an IO Session
	 */
	public IoSession findSession(UUID sessionId) {
		return sessionMap.get(sessionId);
	}

	/**
	 * Converts a text message from MINA into a map containing command
	 * and session information. Called internally by Camel routes.
	 * 
	 * @param exchange the exchange to be filtered
	 */
	public void minaSessionToCommandQueue(Exchange exchange) {
		IoSession session = (IoSession) exchange.getIn().getHeader("CamelMinaIoSession");
		
		// Given a session, construct a map containing key-value pairs of interest
		Map<String, String> body = new HashMap<String, String>();
		addSessionAttributesToMap(session, body);
		body.put("command", exchange.getIn().getBody().toString());
		
		exchange.getIn().setBody(body);
		producer.send(camelEndpointUri, exchange);
	}

	/**
	 * Receives a Map message from the session route and writes
	 * to the corresponding MINA session, optionally closing the
	 * session if directed to do so.
	 * 
	 * @param body a Map of key-value pairs
	 */
	public void sessionQueueToMinaSession(Map<String, String> body) {
		String uuidString = body.remove(SESSION_ID_KEY);
		if(null == uuidString) {
			// No mapping from the supplied session ID to a real session, so punt.
			return;
		}
		
		UUID sessionId = UUID.fromString(uuidString);
		IoSession session = findSession(sessionId);
		if(null == session) {
			// Invalid UUID
			return;
		}
		
		// Send text to the socket, if any is available
		String text = body.remove("text");
		if(null != session && null != text) {
			session.write(text + "\r");
		}

		// Close the socket if requested
		String sessionLifecycle = body.remove("sessionLifecycle");
		if("close".equals(sessionLifecycle)) {
			session.close();
			return;
		}
	
		// Otherwise, any remaining map keys are added to the session as attributes
		addMapToSessionAttributes(body, session);
	}

	/**
	 * Extract interesting session attributes and add them to a map.
	 * Don't include MINA reserved attributes.
	 * 
	 * @param session the session to extract attributes from
	 * @param map the map to add attributes to
	 */
	private void addSessionAttributesToMap(IoSession session, Map<String, String> map) {
		for(String key: session.getAttributeKeys()) {
			if(!key.startsWith(RESERVED_ATTRIBUTE_PREFIX)) {
				map.put(key, session.getAttribute(key).toString());
			}
		}
	}
	
	/**
	 * Extract interesting attributes from a map and add them to a session.
	 * Don't allow callers to override the reserved session ID attribute
	 * or to override MINA-specific settings.
	 * 
	 * @param map the map to extract attributes from
	 * @param session the session to add attributes to
	 */
	private void addMapToSessionAttributes(Map<String, String> map, IoSession session) {
		for(Entry<String, String> entry: map.entrySet()) {
			String key = entry.getKey();
			if(!SESSION_ID_KEY.equals(key) && !key.startsWith(RESERVED_ATTRIBUTE_PREFIX)) {
				session.setAttribute(key, entry.getValue());
			}
		}
	}
}